2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "SLGaimCocoaAdapter.h"
19 #import <Adium/AIAccountControllerProtocol.h>
21 #import <Adium/AIInterfaceControllerProtocol.h>
22 #import <Adium/AILoginControllerProtocol.h>
23 #import "CBGaimAccount.h"
24 #import "CBGaimServicePlugin.h"
25 #import "adiumGaimCore.h"
26 #import "adiumGaimEventloop.h"
27 #import "UndeclaredLibgaimFunctions.h"
28 #import <AIUtilities/AIObjectAdditions.h>
29 #import <Adium/AIAccount.h>
30 #import <Adium/AICorePluginLoader.h>
31 #import <Adium/AIService.h>
32 #import <Adium/AIChat.h>
33 #import <Adium/AIContentTyping.h>
34 #import <Adium/AIHTMLDecoder.h>
35 #import <Adium/AIListContact.h>
36 #import <Adium/NDRunLoopMessenger.h>
38 #import <CoreFoundation/CFRunLoop.h>
39 #import <CoreFoundation/CFSocket.h>
40 #include <Libgaim/libgaim.h>
44 #ifndef JOSCAR_SUPERCEDE_LIBGAIM
45 #import "ESGaimAIMAccount.h"
46 #import "CBGaimOscarAccount.h"
49 //Gaim slash command interface
50 #include <Libgaim/cmds.h>
52 @interface SLGaimCocoaAdapter (PRIVATE)
54 - (BOOL)attemptGaimCommandOnMessage:(NSString *)originalMessage fromAccount:(AIAccount *)sourceAccount inChat:(AIChat *)chat;
55 - (void)refreshAutoreleasePool:(NSTimer *)inTimer;
59 * A pointer to the single instance of this class active in the application.
60 * The gaim callbacks need to be C functions with specific prototypes, so they
61 * can't be ObjC methods. The ObjC callbacks do need to be ObjC methods. This
62 * allows the C ones to call the ObjC ones.
64 static SLGaimCocoaAdapter *sharedInstance = nil;
66 //Dictionaries to track gaim<->adium interactions
67 NSMutableDictionary *accountDict = nil;
68 //NSMutableDictionary *contactDict = nil;
69 NSMutableDictionary *chatDict = nil;
71 //The autorelease pool presently in use; it will be periodically released and recreated
72 static NSAutoreleasePool *currentAutoreleasePool = nil;
73 #define AUTORELEASE_POOL_REFRESH 5.0
75 static NSMutableArray *libgaimPluginArray = nil;
77 @implementation SLGaimCocoaAdapter
80 * @brief Return the shared instance
82 + (SLGaimCocoaAdapter *)sharedInstance
85 if (!sharedInstance) {
86 sharedInstance = [[self alloc] init];
90 return sharedInstance;
94 * @brief Plugin loaded
96 * Initialize each libgaim plugin. These plugins should not do anything within libgaim itself; this should be done in
97 * -[plugin initLibgaimPlugin].
101 NSEnumerator *enumerator;
102 NSString *libgaimPluginPath;
104 libgaimPluginArray = [[NSMutableArray alloc] init];
106 enumerator = [[[AIObject sharedAdiumInstance] allResourcesForName:@"Plugins"
107 withExtensions:@"AdiumLibgaimPlugin"] objectEnumerator];
108 while ((libgaimPluginPath = [enumerator nextObject])) {
109 [AICorePluginLoader loadPluginAtPath:libgaimPluginPath
111 pluginArray:libgaimPluginArray];
115 + (NSArray *)libgaimPluginArray
117 return libgaimPluginArray;
120 //Register the account gaimside in the gaim thread
121 - (void)addAdiumAccount:(CBGaimAccount *)adiumAccount
123 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
124 account->ui_data = [adiumAccount retain];
126 gaim_accounts_add(account);
127 gaim_account_set_status_list(account, "offline", YES, NULL);
130 //Remove an account gaimside
131 - (void)removeAdiumAccount:(CBGaimAccount *)adiumAccount
133 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
135 [(CBGaimAccount *)account->ui_data release];
136 account->ui_data = nil;
138 gaim_accounts_remove(account);
141 #pragma mark Initialization
144 if ((self = [super init])) {
145 accountDict = [[NSMutableDictionary alloc] init];
146 chatDict = [[NSMutableDictionary alloc] init];
155 * @brief Empty and recreate the autorelease pool
157 * Our autoreleased objects will only be released when the outermost autorelease pool is released.
158 * This is handled automatically in the main thread, but we need to do it manually here.
160 - (void)refreshAutoreleasePool:(NSTimer *)inTimer
162 [currentAutoreleasePool release];
163 currentAutoreleasePool = [[NSAutoreleasePool alloc] init];
166 static void ZombieKiller_Signal(int i)
171 while ((child_pid = waitpid(-1, &status, WNOHANG)) > 0);
176 //Set the gaim user directory to be within this user's directory
177 NSString *gaimUserDir = [[[adium loginController] userDirectory] stringByAppendingPathComponent:@"libgaim"];
178 gaim_util_set_user_dir([[gaimUserDir stringByExpandingTildeInPath] UTF8String]);
180 /* Delete blist.xml once when 1.0 runs to clear out any old silliness */
181 if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Adium 1.0 deleted blist.xml"]) {
182 [[NSFileManager defaultManager] removeFileAtPath:
183 [[[NSString stringWithUTF8String:gaim_user_dir()] stringByAppendingPathComponent:@"blist"] stringByAppendingPathExtension:@"xml"]
185 [[NSUserDefaults standardUserDefaults] setBool:YES
186 forKey:@"Adium 1.0 deleted blist.xml"];
189 gaim_core_set_ui_ops(adium_gaim_core_get_ops());
190 gaim_eventloop_set_ui_ops(adium_gaim_eventloop_get_ui_ops());
192 //Initialize the libgaim core; this will call back on the function specified in our core UI ops for us to finish configuring libgaim
193 if (!gaim_core_init("Adium")) {
194 NSLog(@"*** FATAL ***: Failed to initialize gaim core");
195 GaimDebug (@"*** FATAL ***: Failed to initialize gaim core");
198 //Libgaim's async DNS lookup tends to create zombies.
200 struct sigaction act;
202 act.sa_handler = ZombieKiller_Signal;
203 //Send for terminated but not stopped children
204 act.sa_flags = SA_NOCLDWAIT;
206 sigaction(SIGCHLD, &act, NULL);
210 #pragma mark Lookup functions
212 NSString* serviceClassForGaimProtocolID(const char *protocolID)
214 NSString *serviceClass = nil;
216 if (!strcmp(protocolID, "prpl-oscar"))
217 serviceClass = @"AIM-compatible";
218 else if (!strcmp(protocolID, "prpl-gg"))
219 serviceClass = @"Gadu-Gadu";
220 else if (!strcmp(protocolID, "prpl-jabber"))
221 serviceClass = @"Jabber";
222 else if (!strcmp(protocolID, "prpl-meanwhile"))
223 serviceClass = @"Sametime";
224 else if (!strcmp(protocolID, "prpl-msn"))
225 serviceClass = @"MSN";
226 else if (!strcmp(protocolID, "prpl-novell"))
227 serviceClass = @"GroupWise";
228 else if (!strcmp(protocolID, "prpl-yahoo"))
229 serviceClass = @"Yahoo!";
230 else if (!strcmp(protocolID, "prpl-zephyr"))
231 serviceClass = @"Zephyr";
238 * Finds an instance of CBGaimAccount for a pointer to a GaimAccount struct.
240 CBGaimAccount* accountLookup(GaimAccount *acct)
242 CBGaimAccount *adiumGaimAccount = (acct ? (CBGaimAccount *)acct->ui_data : nil);
243 /* If the account doesn't have its ui_data associated yet (we haven't tried to connect) but we want this
244 * lookup data, we have to do some manual parsing. This is used for example from the OTR preferences.
246 if (!adiumGaimAccount && acct) {
247 const char *protocolID = acct->protocol_id;
248 NSString *serviceClass = serviceClassForGaimProtocolID(protocolID);
250 NSEnumerator *enumerator = [[[[AIObject sharedAdiumInstance] accountController] accounts] objectEnumerator];
251 while ((adiumGaimAccount = [enumerator nextObject])) {
252 if ([adiumGaimAccount isKindOfClass:[CBGaimAccount class]] &&
253 [[[adiumGaimAccount service] serviceClass] isEqualToString:serviceClass] &&
254 [[adiumGaimAccount UID] caseInsensitiveCompare:[NSString stringWithUTF8String:acct->username]] == NSOrderedSame) {
259 return adiumGaimAccount;
262 GaimAccount* accountLookupFromAdiumAccount(CBGaimAccount *adiumAccount)
264 return [adiumAccount gaimAccount];
267 AIListContact* contactLookupFromBuddy(GaimBuddy *buddy)
269 //Get the node's ui_data
270 AIListContact *theContact = (buddy ? (AIListContact *)buddy->node.ui_data : nil);
272 //If the node does not have ui_data yet, we need to create a contact and associate it
273 if (!theContact && buddy) {
276 UID = [NSString stringWithUTF8String:gaim_normalize(buddy->account, buddy->name)];
278 theContact = [accountLookup(buddy->account) contactWithUID:UID];
280 //Associate the handle with ui_data and the buddy with our statusDictionary
281 buddy->node.ui_data = [theContact retain];
287 AIListContact* contactLookupFromIMConv(GaimConversation *conv)
292 AIChat* groupChatLookupFromConv(GaimConversation *conv)
296 chat = (AIChat *)conv->ui_data;
298 NSString *name = [NSString stringWithUTF8String:conv->name];
300 chat = [accountLookup(conv->account) chatWithName:name];
302 [chatDict setObject:[NSValue valueWithPointer:conv] forKey:[chat uniqueChatID]];
303 conv->ui_data = [chat retain];
309 AIChat* existingChatLookupFromConv(GaimConversation *conv)
311 return (conv ? conv->ui_data : nil);
314 AIChat* chatLookupFromConv(GaimConversation *conv)
316 switch(gaim_conversation_get_type(conv)) {
317 case GAIM_CONV_TYPE_CHAT:
318 return groupChatLookupFromConv(conv);
320 case GAIM_CONV_TYPE_IM:
321 return imChatLookupFromConv(conv);
324 return existingChatLookupFromConv(conv);
329 AIChat* imChatLookupFromConv(GaimConversation *conv)
333 chat = (AIChat *)conv->ui_data;
336 //No chat is associated with the IM conversation
337 AIListContact *sourceContact;
339 GaimAccount *account;
341 account = conv->account;
342 // GaimDebug (@"%x conv->name %s; normalizes to %s",account,conv->name,gaim_normalize(account,conv->name));
344 //First, find the GaimBuddy with whom we are conversing
345 buddy = gaim_find_buddy(account, conv->name);
347 GaimDebug (@"imChatLookupFromConv: Creating %s %s",account->username,gaim_normalize(account,conv->name));
348 //No gaim_buddy corresponding to the conv->name is on our list, so create one
349 buddy = gaim_buddy_new(account, gaim_normalize(account, conv->name), NULL); //create a GaimBuddy
352 NSCAssert(buddy != nil, @"buddy was nil");
354 sourceContact = contactLookupFromBuddy(buddy);
356 // Need to start a new chat, associating with the GaimConversation
357 chat = [accountLookup(account) chatWithContact:sourceContact];
360 NSString *errorString;
362 errorString = [NSString stringWithFormat:@"conv %x: Got nil chat in lookup for sourceContact %@ (%x ; \"%s\" ; \"%s\") on adiumAccount %@ (%x ; \"%s\")",
366 (buddy ? buddy->name : ""),
367 ((buddy && buddy->account && buddy->name) ? gaim_normalize(buddy->account, buddy->name) : ""),
368 accountLookup(account),
370 (account ? account->username : "")];
372 NSCAssert(chat != nil, errorString);
375 //Associate the GaimConversation with the AIChat
376 [chatDict setObject:[NSValue valueWithPointer:conv] forKey:[chat uniqueChatID]];
377 conv->ui_data = [chat retain];
383 GaimConversation* convLookupFromChat(AIChat *chat, id adiumAccount)
385 GaimConversation *conv = [[chatDict objectForKey:[chat uniqueChatID]] pointerValue];
386 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
388 if (!conv && adiumAccount) {
389 AIListObject *listObject = [chat listObject];
391 //If we have a listObject, we are dealing with a one-on-one chat, so proceed accordingly
395 destination = g_strdup(gaim_normalize(account, [[listObject UID] UTF8String]));
397 conv = gaim_conversation_new(GAIM_CONV_TYPE_IM, account, destination);
399 //associate the AIChat with the gaim conv
400 if (conv) imChatLookupFromConv(conv);
405 //Otherwise, we have a multiuser chat.
407 //All multiuser chats should have a non-nil name.
408 NSString *chatName = [chat name];
410 const char *name = [chatName UTF8String];
413 Look for an existing gaimChat. If we find one, our job is complete.
415 We will never find one if we are joining a chat on our own (via the Join Chat dialogue).
417 We should never get to this point if we were invited to a chat, as groupChatLookupFromConv(),
418 which was called when we accepted the invitation and got the chat information from Gaim,
419 will have associated the GaimConversation with the chat and we would have stopped after
420 [[chatDict objectForKey:[chat uniqueChatID]] pointerValue] above.
422 However, there's no reason not to check just in case.
424 GaimChat *gaimChat = gaim_blist_find_chat (account, name);
428 If we don't have a GaimChat with this name on this account, we need to create one.
429 Our chat, which should have been created via the Adium Join Chat API, should have
430 a ChatCreationInfo status object with the information we need to ask Gaim to
433 NSDictionary *chatCreationInfo = [chat statusObjectForKey:@"ChatCreationInfo"];
435 GaimDebug (@"Creating a chat.");
437 GHashTable *components;
440 GaimConnection *gc = gaim_account_get_connection(account);
442 struct proto_chat_entry *pce;
443 NSString *identifier;
444 NSEnumerator *enumerator;
446 //Create a hash table
447 //The hash table should contain char* objects created via a g_strdup method
448 components = g_hash_table_new_full(g_str_hash, g_str_equal,
451 enumerator = [chatCreationInfo keyEnumerator];
452 while ((identifier = [enumerator nextObject])) {
453 id value = [chatCreationInfo objectForKey:identifier];
454 char *valueUTF8String = NULL;
456 if ([value isKindOfClass:[NSNumber class]]) {
457 valueUTF8String = g_strdup_printf("%d",[value intValue]);
459 } else if ([value isKindOfClass:[NSString class]]) {
460 valueUTF8String = g_strdup([value UTF8String]);
463 GaimDebug (@"Invalid value %@ for identifier %@",value,identifier);
466 //Store our chatCreationInfo-supplied value in the compnents hash table
467 if (valueUTF8String) {
468 g_hash_table_replace(components,
469 g_strdup([identifier UTF8String]),
474 //In debug mode, verify we didn't miss any required values
477 Get the chat_info for our desired account. This will be a GList of proto_chat_entry
478 objects, each of which has a label and identifier. Each may also have is_int, with a minimum
479 and a maximum integer value.
481 if ((GAIM_PLUGIN_PROTOCOL_INFO(gc->prpl))->chat_info)
483 list = (GAIM_PLUGIN_PROTOCOL_INFO(gc->prpl))->chat_info(gc);
485 //Look at each proto_chat_entry in the list to verify we have it in chatCreationInfo
486 for (tmp = list; tmp; tmp = tmp->next)
489 char *identifier = g_strdup(pce->identifier);
491 NSString *value = [chatCreationInfo objectForKey:[NSString stringWithUTF8String:identifier]];
493 GaimDebug (@"Danger, Will Robinson! %s is in the proto_info but can't be found in %@",identifier,chatCreationInfo);
500 //Add the GaimChat to our local buddy list?
501 gaimChat = gaim_chat_new(account,
504 if ((group = gaim_find_group(group_name)) == NULL) {
505 group = gaim_group_new(group_name);
506 gaim_blist_add_group(group, NULL);
509 if (gaimChat != NULL) {
510 gaim_blist_add_chat(gaimChat, group, NULL);
514 //Join the chat serverside - the GHashTable components, couple with the originating GaimConnect,
515 //now contains all the information the prpl will need to process our request.
516 GaimDebug (@"In the event of an emergency, your GHashTable may be used as a flotation device...");
517 serv_join_chat(gc, components);
526 GaimConversation* existingConvLookupFromChat(AIChat *chat)
528 return (GaimConversation *)[[chatDict objectForKey:[chat uniqueChatID]] pointerValue];
531 void* adium_gaim_get_handle(void)
533 static int adium_gaim_handle;
535 return &adium_gaim_handle;
538 NSMutableDictionary* get_chatDict(void)
545 static NSString* _messageImageCachePath(int imageID, AIAccount* adiumAccount)
547 NSString *messageImageCacheFilename = [NSString stringWithFormat:@"TEMP-Image_%@_%i", [adiumAccount internalObjectID], imageID];
548 return [[[[AIObject sharedAdiumInstance] cachesPath] stringByAppendingPathComponent:messageImageCacheFilename] stringByAppendingPathExtension:@"png"];
551 NSString* processGaimImages(NSString* inString, AIAccount* adiumAccount)
554 NSString *chunkString = nil;
555 NSMutableString *newString;
556 NSString *targetString = @"<IMG ID=";
557 NSCharacterSet *quoteApostropheCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"\"\'"];
560 if ([inString rangeOfString:targetString options:NSCaseInsensitiveSearch].location == NSNotFound) {
565 newString = [[NSMutableString alloc] init];
567 scanner = [NSScanner scannerWithString:inString];
568 [scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@""]];
570 //A gaim image tag takes the form <IMG ID='12'></IMG> where 12 is the reference for use in GaimStoredImage* gaim_imgstore_get(int)
572 //Parse the incoming HTML
573 while (![scanner isAtEnd]) {
575 //Find the beginning of a gaim IMG ID tag
576 if ([scanner scanUpToString:targetString intoString:&chunkString]) {
577 [newString appendString:chunkString];
580 if ([scanner scanString:targetString intoString:&chunkString]) {
581 //Skip past a quote or apostrophe
582 [scanner scanCharactersFromSet:quoteApostropheCharacterSet intoString:NULL];
584 //Get the image ID from the tag
585 [scanner scanInt:&imageID];
587 //Skip past a quote or apostrophe
588 [scanner scanCharactersFromSet:quoteApostropheCharacterSet intoString:NULL];
591 [scanner scanString:@">" intoString:nil];
593 //Get the image, then write it out as a png
594 GaimStoredImage *gaimImage = gaim_imgstore_get(imageID);
596 NSString *filename = (gaim_imgstore_get_filename(gaimImage) ?
597 [NSString stringWithUTF8String:gaim_imgstore_get_filename(gaimImage)] :
599 NSString *imagePath = _messageImageCachePath(imageID, adiumAccount);
601 //First make an NSImage, then request a TIFFRepresentation to avoid an obscure bug in the PNG writing routines
602 //Exception: PNG writer requires compacted components (bits/component * components/pixel = bits/pixel)
603 NSImage *image = [[NSImage alloc] initWithData:[NSData dataWithBytes:gaim_imgstore_get_data(gaimImage)
604 length:gaim_imgstore_get_size(gaimImage)]];
605 NSData *imageTIFFData = [image TIFFRepresentation];
606 NSBitmapImageRep *bitmapRep = [NSBitmapImageRep imageRepWithData:imageTIFFData];
608 //If writing the PNG file is successful, write an <IMG SRC="filepath"> tag to our string; the 'scaledToFitImage' class lets us apply CSS to directIM images only
609 if ([[bitmapRep representationUsingType:NSPNGFileType properties:nil] writeToFile:imagePath atomically:YES]) {
610 [newString appendString:[NSString stringWithFormat:@"<IMG CLASS=\"scaledToFitImage\" SRC=\"%@\" ALT=\"%@\">",
611 imagePath, filename]];
616 //If we didn't get a gaimImage, just leave the tag for now.. maybe it was important?
617 [newString appendFormat:@"<IMG ID=\"%i\">",chunkString];
622 return ([newString autorelease]);
626 // Notify ----------------------------------------------------------------------------------------------------------
627 // We handle the notify messages within SLGaimCocoaAdapter so we can use our localized string macro
628 - (void *)handleNotifyMessageOfType:(GaimNotifyType)type withTitle:(const char *)title primary:(const char *)primary secondary:(const char *)secondary;
630 NSString *primaryString = [NSString stringWithUTF8String:primary];
631 NSString *secondaryString = secondary ? [NSString stringWithUTF8String:secondary] : nil;
633 NSString *titleString;
635 titleString = [NSString stringWithFormat:AILocalizedString(@"Adium Notice: %@",nil),[NSString stringWithUTF8String:title]];
637 titleString = AILocalizedString(@"Adium : Notice", nil);
640 NSString *errorMessage = nil;
641 NSString *description = nil;
644 if (([primaryString rangeOfString:@"Already there"].location != NSNotFound)) {
645 return adium_gaim_get_handle();
649 //Suppress notification warnings we have no interest in seeing
650 if (secondaryString) {
651 if (([secondaryString rangeOfString:@"Could not add the buddy 1 for an unknown reason"].location != NSNotFound) ||
652 ([secondaryString rangeOfString:@"Your screen name is currently formatted as follows"].location != NSNotFound) ||
653 ([secondaryString rangeOfString:@"Error reading from Switchboard server"].location != NSNotFound) ||
654 ([secondaryString rangeOfString:@"0x001a: Unknown error"].location != NSNotFound) ||
655 ([secondaryString rangeOfString:@"Not supported by host"].location != NSNotFound) ||
656 ([secondaryString rangeOfString:@"Not logged in"].location != NSNotFound)) {
657 return adium_gaim_get_handle();
661 if ([primaryString rangeOfString: @"Yahoo! message did not get sent."].location != NSNotFound) {
663 errorMessage = AILocalizedString(@"Your Yahoo! message did not get sent.", nil);
665 } else if ([primaryString rangeOfString: @"did not get sent"].location != NSNotFound) {
667 NSString *targetUserName = [[[[primaryString componentsSeparatedByString:@" message to "] objectAtIndex:1] componentsSeparatedByString:@" did not get "] objectAtIndex:0];
669 errorMessage = [NSString stringWithFormat:AILocalizedString(@"Your message to %@ did not get sent",nil),targetUserName];
671 if ([secondaryString rangeOfString:@"Rate"].location != NSNotFound) {
672 description = AILocalizedString(@"You are sending messages too quickly; wait a moment and try again.",nil);
673 } else if ([secondaryString isEqualToString:@"Service unavailable"] || [secondaryString isEqualToString:@"Not logged in"]) {
674 description = AILocalizedString(@"Connection error.",nil);
675 } else if ([secondaryString isEqualToString:@"Refused by client"]) {
676 description = AILocalizedString(@"Your message was refused by the other user.",nil);
677 } else if ([secondaryString isEqualToString:@"Reply too big"]) {
678 description = AILocalizedString(@"Your message was too big.",nil);
679 } else if ([secondaryString isEqualToString:@"In local permit/deny"]) {
680 description = AILocalizedString(@"The other user is in your deny list.",nil);
681 } else if ([secondaryString rangeOfString:@"Too evil"].location != NSNotFound) {
682 description = AILocalizedString(@"Warning level is too high.",nil);
683 } else if ([secondaryString isEqualToString:@"User temporarily unavailable"]) {
684 description = AILocalizedString(@"The other user is temporarily unavailable.",nil);
686 description = AILocalizedString(@"No reason was given.",nil);
689 } else if ([primaryString rangeOfString: @"Authorization Denied"].location != NSNotFound) {
690 //Authorization denied; grab the user name and reason
691 NSArray *parts = [[[secondaryString componentsSeparatedByString:@" user "] objectAtIndex:1] componentsSeparatedByString:@" has denied your request to add them to your buddy list for the following reason:\n"];
692 NSString *targetUserName = [parts objectAtIndex:0];
693 NSString *reason = ([parts count] > 1 ? [parts objectAtIndex:1] : AILocalizedString(@"(No reason given)",nil));
695 errorMessage = [NSString stringWithFormat:AILocalizedString(@"%@ denied authorization:","User deined authorization; the next line has an explanation."),targetUserName];
696 description = reason;
698 } else if ([primaryString rangeOfString: @"Authorization Granted"].location != NSNotFound) {
699 //ICQ Authorization granted
700 NSString *targetUserName = [[[[secondaryString componentsSeparatedByString:@" user "] objectAtIndex:1] componentsSeparatedByString:@" has "] objectAtIndex:0];
702 errorMessage = [NSString stringWithFormat:AILocalizedString(@"%@ granted authorization.",nil),targetUserName];
705 //If we didn't grab a translated version, at least display the English version Gaim supplied
706 [[adium interfaceController] handleMessage:([errorMessage length] ? errorMessage : primaryString)
707 withDescription:([description length] ? description : ([secondaryString length] ? secondaryString : @"") )
708 withWindowTitle:titleString];
714 - (void *)handleNotifyFormattedWithTitle:(const char *)title primary:(const char *)primary secondary:(const char *)secondary text:(const char *)text
716 NSString *titleString = (title ? [NSString stringWithUTF8String:title] : nil);
717 NSString *primaryString = (primary ? [NSString stringWithUTF8String:primary] : nil);
720 titleString = primaryString;
724 NSString *secondaryString = (secondary ? [NSString stringWithUTF8String:secondary] : nil);
725 if (!primaryString) {
726 primaryString = secondaryString;
727 secondaryString = nil;
730 static AIHTMLDecoder *notifyFormattedHTMLDecoder = nil;
731 if (!notifyFormattedHTMLDecoder) notifyFormattedHTMLDecoder = [[AIHTMLDecoder decoder] retain];
733 NSString *textString = (text ? [NSString stringWithUTF8String:text] : nil);
734 if (textString) textString = [[notifyFormattedHTMLDecoder decodeHTML:textString] string];
736 NSString *description = nil;
737 if ([textString length] && [secondaryString length]) {
738 description = [NSString stringWithFormat:@"%@\n\n%@",secondaryString,textString];
740 } else if (textString) {
741 description = textString;
743 } else if (secondaryString) {
744 description = secondaryString;
748 NSString *message = primaryString;
750 [[adium interfaceController] handleMessage:(message ? message : @"")
751 withDescription:(description ? description : @"")
752 withWindowTitle:(titleString ? titleString : @"")];
758 #pragma mark File transfers
759 - (void)displayFileSendError
761 [[adium interfaceController] handleMessage:AILocalizedString(@"File Send Error",nil)
762 withDescription:AILocalizedString(@"An error was encoutered sending the file.",nil)
763 withWindowTitle:AILocalizedString(@"File Send Error",nil)];
766 #pragma mark Thread accessors
767 - (void)disconnectAccount:(id)adiumAccount
769 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
770 AILog(@"Setting %x disabled and offline (%s)...",account,
771 gaim_status_type_get_id(gaim_account_get_status_type_with_primitive(account, GAIM_STATUS_OFFLINE)));
773 gaim_account_set_enabled(account, "Adium", NO);
776 - (void)registerAccount:(id)adiumAccount
778 gaim_account_register(accountLookupFromAdiumAccount(adiumAccount));
781 //Called on the gaim thread, actually performs the specified command (it should have already been tested by
782 //attemptGaimCommandOnMessage:... above.
783 - (BOOL)doCommand:(NSString *)originalMessage
784 fromAccount:(id)sourceAccount
785 inChat:(AIChat *)chat
787 GaimConversation *conv = convLookupFromChat(chat, sourceAccount);
788 GaimCmdStatus status;
789 char *markup, *error;
791 BOOL didCommand = NO;
793 cmd = [originalMessage UTF8String];
795 //cmd+1 will be the cmd without the leading character, which should be "/"
796 markup = g_markup_escape_text(cmd+1, -1);
797 status = gaim_cmd_do_command(conv, cmd+1, markup, &error);
799 //The only error status which is possible now is either
801 case GAIM_CMD_STATUS_FAILED:
803 gaim_conv_present_error(conv->name, conv->account, "Command failed");
807 case GAIM_CMD_STATUS_WRONG_ARGS:
809 gaim_conv_present_error(conv->name, conv->account, "Wrong number of arguments");
813 case GAIM_CMD_STATUS_OK:
816 case GAIM_CMD_STATUS_NOT_FOUND:
817 case GAIM_CMD_STATUS_WRONG_TYPE:
818 case GAIM_CMD_STATUS_WRONG_PRPL:
819 /* Ignore this command and let the message send; the user probably doesn't even know what they typed is a command */
828 * @brief Check a message for gaim / commands=
830 * @result YES if a command was performed; NO if it was not
832 - (BOOL)attemptGaimCommandOnMessage:(NSString *)originalMessage fromAccount:(AIAccount *)sourceAccount inChat:(AIChat *)chat
834 BOOL didCommand = NO;
836 if ([originalMessage hasPrefix:@"/"]) {
837 didCommand = [self doCommand:originalMessage
838 fromAccount:sourceAccount
845 //Returns YES if the message was sent (and should therefore be displayed). Returns NO if it was not sent or was otherwise used.
846 - (void)sendEncodedMessage:(NSString *)encodedMessage
847 fromAccount:(id)sourceAccount
848 inChat:(AIChat *)chat
849 withFlags:(GaimMessageFlags)flags
851 const char *encodedMessageUTF8String;
853 if (encodedMessage && (encodedMessageUTF8String = [encodedMessage UTF8String])) {
854 GaimConversation *conv = convLookupFromChat(chat,sourceAccount);
856 switch (gaim_conversation_get_type(conv)) {
857 case GAIM_CONV_TYPE_IM: {
858 GaimConvIm *im = gaim_conversation_get_im_data(conv);
859 gaim_conv_im_send_with_flags(im, encodedMessageUTF8String, flags);
863 case GAIM_CONV_TYPE_CHAT: {
864 GaimConvChat *gaimChat = gaim_conversation_get_chat_data(conv);
865 gaim_conv_chat_send(gaimChat, encodedMessageUTF8String);
869 case GAIM_CONV_TYPE_ANY:
870 GaimDebug (@"What in the world? Got GAIM_CONV_TYPE_ANY.");
873 case GAIM_CONV_TYPE_MISC:
874 case GAIM_CONV_TYPE_UNKNOWN:
878 GaimDebug (@"*** Error encoding %@ to UTF8",encodedMessage);
882 - (void)sendTyping:(AITypingState)typingState inChat:(AIChat *)chat
884 GaimConversation *conv = convLookupFromChat(chat,nil);
886 // BOOL isTyping = (([typingState intValue] == AINotTyping) ? FALSE : TRUE);
888 GaimTypingState gaimTypingState;
890 switch (typingState) {
893 gaimTypingState = GAIM_NOT_TYPING;
896 gaimTypingState = GAIM_TYPING;
899 gaimTypingState = GAIM_TYPED;
903 serv_send_typing(gaim_conversation_get_gc(conv),
904 gaim_conversation_get_name(conv),
909 - (void)addUID:(NSString *)objectUID onAccount:(id)adiumAccount toGroup:(NSString *)groupName
911 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
912 const char *groupUTF8String, *buddyUTF8String;
916 //Find the group (Create if necessary)
917 groupUTF8String = (groupName ? [groupName UTF8String] : "Buddies");
918 if (!(group = gaim_find_group(groupUTF8String))) {
919 group = gaim_group_new(groupUTF8String);
920 gaim_blist_add_group(group, NULL);
923 //Find the buddy (Create if necessary)
924 buddyUTF8String = [objectUID UTF8String];
925 buddy = gaim_find_buddy(account, buddyUTF8String);
926 if (!buddy) buddy = gaim_buddy_new(account, buddyUTF8String, NULL);
928 GaimDebug (@"Adding buddy %s to group %s",buddy->name, group->name);
930 /* gaim_blist_add_buddy() will move an existing contact serverside, but will not add a buddy serverside.
931 * We're working with a new contact, hopefully, so we want to call serv_add_buddy() after modifying the gaim list.
932 * This is the order done in add_buddy_cb() in gtkblist.c */
933 gaim_blist_add_buddy(buddy, NULL, group, NULL);
934 gaim_account_add_buddy(account, buddy);
937 - (void)removeUID:(NSString *)objectUID onAccount:(id)adiumAccount fromGroup:(NSString *)groupName
939 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
942 if ((buddy = gaim_find_buddy(account, [objectUID UTF8String]))) {
943 const char *groupUTF8String;
946 groupUTF8String = (groupName ? [groupName UTF8String] : "Buddies");
947 if ((group = gaim_find_group(groupUTF8String))) {
948 /* Remove this contact from the server-side and gaim-side lists.
949 * Updating gaimside does not change the server.
951 * Gaim has a commented XXX as to whether this order or the reverse (blist, then serv) is correct.
952 * We'll use the order which gaim uses as of gaim 1.1.4. */
953 gaim_account_remove_buddy(account, buddy, group);
954 gaim_blist_remove_buddy(buddy);
959 - (void)moveUID:(NSString *)objectUID onAccount:(id)adiumAccount toGroup:(NSString *)groupName
961 GaimAccount *account;
964 const char *buddyUTF8String;
965 const char *groupUTF8String;
967 account = accountLookupFromAdiumAccount(adiumAccount);
969 //Get the destination group (creating if necessary)
970 groupUTF8String = (groupName ? [groupName UTF8String] : "Buddies");
971 group = gaim_find_group(groupUTF8String);
973 /* If we can't find the group, something's gone wrong... we shouldn't be using a group we don't have.
974 * We'll just silently turn this into an add operation. */
975 group = gaim_group_new(groupUTF8String);
976 gaim_blist_add_group(group, NULL);
979 buddyUTF8String = [objectUID UTF8String];
980 /* If we support contacts in multiple groups at once this should change */
981 GSList *buddies = gaim_find_buddies(account, buddyUTF8String);
985 for (cur = buddies; cur; cur = cur->next) {
986 /* gaim_blist_add_buddy() will update the local list and perform a serverside move as necessary */
987 gaim_blist_add_buddy(cur->data, NULL, group, NULL);
991 /* If we can't find a buddy, something's gone wrong... we shouldn't be moving a buddy we don't have.
992 * As with the group, we'll just silently turn this into an add operation. */
993 buddy = gaim_buddy_new(account, buddyUTF8String, NULL);
995 /* gaim_blist_add_buddy() will update the local list and perform a serverside move as necessary */
996 gaim_blist_add_buddy(buddy, NULL, group, NULL);
998 /* gaim_blist_add_buddy() won't perform a serverside add, however. Add if necessary. */
999 gaim_account_add_buddy(account, buddy);
1003 - (void)renameGroup:(NSString *)oldGroupName onAccount:(id)adiumAccount to:(NSString *)newGroupName
1005 GaimGroup *group = gaim_find_group([oldGroupName UTF8String]);
1007 //If we don't have a group with this name, just ignore the rename request
1009 //Rename gaimside, which will rename serverside as well
1010 gaim_blist_rename_group(group, [newGroupName UTF8String]);
1014 - (void)deleteGroup:(NSString *)groupName onAccount:(id)adiumAccount
1016 GaimGroup *group = gaim_find_group([groupName UTF8String]);
1019 gaim_blist_remove_group(group);
1024 - (void)setAlias:(NSString *)alias forUID:(NSString *)UID onAccount:(id)adiumAccount
1026 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
1027 if (gaim_account_is_connected(account)) {
1028 const char *uidUTF8String = [UID UTF8String];
1029 GaimBuddy *buddy = gaim_find_buddy(account, uidUTF8String);
1030 const char *aliasUTF8String = [alias UTF8String];
1031 const char *oldAlias = (buddy ? gaim_buddy_get_alias(buddy) : nil);
1033 if (buddy && ((aliasUTF8String && !oldAlias) ||
1034 (!aliasUTF8String && oldAlias) ||
1035 ((oldAlias && aliasUTF8String && (strcmp(oldAlias,aliasUTF8String) != 0))))) {
1037 gaim_blist_alias_buddy(buddy,aliasUTF8String);
1038 serv_alias_buddy(buddy);
1040 //If we had an alias before but no longer have, adiumGaimBlistUpdate() is not going to send the update
1041 //(Because normally it's wasteful to send a nil alias to the account). We need to manually invoke the update.
1042 if (oldAlias && !alias) {
1043 AIListContact *theContact = contactLookupFromBuddy(buddy);
1045 [adiumAccount updateContact:theContact
1053 - (void)openChat:(AIChat *)chat onAccount:(id)adiumAccount
1055 //Looking up the conv from the chat will create the GaimConversation gaimside, joining the chat, opening the server
1056 //connection, or whatever else is done when a chat is opened.
1057 convLookupFromChat(chat,adiumAccount);
1060 - (void)closeChat:(AIChat *)chat
1062 GaimConversation *conv = existingConvLookupFromChat(chat);
1065 //We use chatDict's objectfor the passed chatUniqueID because we can no longer trust any other
1066 //values due to threading potentially letting them have changed on us.
1067 [chatDict removeObjectForKey:[chat uniqueChatID]];
1069 //We retained the chat when setting it as the ui_data; we are releasing here, so be sure to set conv->ui_data
1070 //to nil so we don't try to do it again.
1071 [(AIChat *)conv->ui_data release];
1072 conv->ui_data = nil;
1074 //Tell gaim to destroy the conversation.
1075 gaim_conversation_destroy(conv);
1079 - (void)inviteContact:(AIListContact *)listContact toChat:(AIChat *)chat withMessage:(NSString *)inviteMessage;
1081 GaimConversation *conv;
1082 GaimAccount *account;
1083 GaimConvChat *gaimChat;
1084 AIAccount *adiumAccount = [chat account];
1086 GaimDebug (@"#### inviteContact:%@ toChat:%@",[listContact UID],[chat name]);
1088 if (([adiumAccount isKindOfClass:[CBGaimAccount class]]) &&
1089 (conv = convLookupFromChat(chat, adiumAccount)) &&
1090 (account = accountLookupFromAdiumAccount((CBGaimAccount *)adiumAccount)) &&
1091 (gaimChat = gaim_conversation_get_chat_data(conv))) {
1093 //GaimBuddy *buddy = gaim_find_buddy(account, [[listObject UID] UTF8String]);
1094 GaimDebug (@"#### addChatUser chat: %@ (%@) buddy: %@",[chat name], chat,[listContact UID]);
1095 serv_chat_invite(gaim_conversation_get_gc(conv),
1096 gaim_conv_chat_get_id(gaimChat),
1097 (inviteMessage ? [inviteMessage UTF8String] : ""),
1098 [[listContact UID] UTF8String]);
1103 - (void)createNewGroupChat:(AIChat *)chat withListContact:(AIListContact *)contact
1106 convLookupFromChat(chat, [chat account]);
1108 //Invite the contact, with no message
1109 [self inviteContact:contact toChat:chat withMessage:nil];
1112 #pragma mark Account Status
1113 - (void)setStatusID:(const char *)statusID
1114 isActive:(NSNumber *)isActive
1115 arguments:(NSMutableDictionary *)arguments
1116 onAccount:(id)adiumAccount
1118 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
1119 GList *attrs = NULL;
1121 //Generate a GList of attrs from arguments
1122 if ([arguments count]) {
1123 NSEnumerator *enumerator;
1126 enumerator = [arguments keyEnumerator];
1127 while ((key = [enumerator nextObject])) {
1128 const char *value = NULL;
1131 valueObject = [arguments objectForKey:key];
1133 if ([valueObject isKindOfClass:[NSNumber class]]) {
1134 value = GINT_TO_POINTER([valueObject intValue]);
1136 } else if ([valueObject isKindOfClass:[NSString class]]) {
1137 value = [valueObject UTF8String];
1142 attrs = g_list_append(attrs, (gpointer)[key UTF8String]);
1144 //Now append the value
1145 attrs = g_list_append(attrs, (gpointer)value);
1148 AILog(@"Warning; could not determine value of %@ for key %@, statusID %s",valueObject,key,statusID);
1153 AILog(@"Setting status on %x (%s): ID %s, isActive %i, attributes %@",account, gaim_account_get_username(account),
1154 statusID, [isActive boolValue], arguments);
1155 gaim_account_set_status_list(account, statusID, [isActive boolValue], attrs);
1157 if (gaim_status_is_online(gaim_account_get_active_status(account)) &&
1158 gaim_account_is_disconnected(account)) {
1159 //This status is an online status, but the account is not connected or connecting
1161 //Ensure the account is enabled
1162 if (!gaim_account_get_enabled(account, "Adium")) {
1163 gaim_account_set_enabled(account, "Adium", YES);
1166 //Now connect the account
1167 gaim_account_connect(account);
1172 - (void)setInfo:(NSString *)profileHTML onAccount:(id)adiumAccount
1174 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
1175 const char *profileHTMLUTF8 = [profileHTML UTF8String];
1177 gaim_account_set_user_info(account, profileHTMLUTF8);
1179 if (account->gc != NULL && gaim_account_is_connected(account)) {
1180 serv_set_info(account->gc, profileHTMLUTF8);
1184 - (void)setBuddyIcon:(NSString *)buddyImageFilename onAccount:(id)adiumAccount
1186 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
1188 gaim_account_set_buddy_icon(account, [buddyImageFilename UTF8String]);
1192 - (void)setIdleSinceTo:(NSDate *)idleSince onAccount:(id)adiumAccount
1194 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
1195 if (gaim_account_is_connected(account)) {
1196 NSTimeInterval idle = (idleSince != nil ? [idleSince timeIntervalSince1970] : 0);
1197 GaimPresence *presence;
1199 presence = gaim_account_get_presence(account);
1201 gaim_presence_set_idle(presence, (idle > 0), idle);
1205 #pragma mark Get Info
1206 - (void)getInfoFor:(NSString *)inUID onAccount:(id)adiumAccount
1208 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
1209 if (gaim_account_is_connected(account)) {
1211 serv_get_info(account->gc, [inUID UTF8String]);
1216 - (void)xferRequest:(GaimXfer *)xfer
1218 gaim_xfer_request(xfer);
1221 - (void)xferRequestAccepted:(GaimXfer *)xfer withFileName:(NSString *)xferFileName
1223 //Only start the file transfer if it's still not marked as cancelled and therefore can be begun.
1224 if ((gaim_xfer_get_status(xfer) != GAIM_XFER_STATUS_CANCEL_LOCAL) &&
1225 (gaim_xfer_get_status(xfer) != GAIM_XFER_STATUS_CANCEL_REMOTE)) {
1226 //XXX should do further error checking as done by gaim_xfer_choose_file_ok_cb() in gaim's ft.c
1227 gaim_xfer_request_accepted(xfer, [xferFileName UTF8String]);
1231 - (void)xferRequestRejected:(GaimXfer *)xfer
1233 gaim_xfer_request_denied(xfer);
1236 - (void)xferCancel:(GaimXfer *)xfer
1238 if ((gaim_xfer_get_status(xfer) == GAIM_XFER_STATUS_UNKNOWN) ||
1239 (gaim_xfer_get_status(xfer) == GAIM_XFER_STATUS_NOT_STARTED) ||
1240 (gaim_xfer_get_status(xfer) == GAIM_XFER_STATUS_STARTED) ||
1241 (gaim_xfer_get_status(xfer) == GAIM_XFER_STATUS_ACCEPTED)) {
1242 gaim_xfer_cancel_local(xfer);
1246 #pragma mark Account settings
1247 - (void)setCheckMail:(NSNumber *)checkMail forAccount:(id)adiumAccount
1249 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
1250 BOOL shouldCheckMail = [checkMail boolValue];
1252 gaim_account_set_check_mail(account, shouldCheckMail);
1255 - (void)setDefaultPermitDenyForAccount:(id)adiumAccount
1257 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
1259 if (account && gaim_account_get_connection(account)) {
1260 account->perm_deny = GAIM_PRIVACY_DENY_USERS;
1261 serv_set_permit_deny(gaim_account_get_connection(account));
1265 #pragma mark Protocol specific accessors
1266 #ifndef JOSCAR_SUPERCEDE_LIBGAIM
1267 - (void)OSCAREditComment:(NSString *)comment forUID:(NSString *)inUID onAccount:(id)adiumAccount
1269 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
1270 if (gaim_account_is_connected(account)) {
1275 const char *uidUTF8String = [inUID UTF8String];
1277 if ((buddy = gaim_find_buddy(account, uidUTF8String)) &&
1278 (g = gaim_buddy_get_group(buddy)) &&
1279 (od = account->gc->proto_data)) {
1280 aim_ssi_editcomment(od, g->name, uidUTF8String, [comment UTF8String]);
1285 - (void)OSCARSetFormatTo:(NSString *)inFormattedUID onAccount:(id)adiumAccount
1287 GaimAccount *account = accountLookupFromAdiumAccount(adiumAccount);
1290 gaim_account_is_connected(account) &&
1291 [inFormattedUID length]) {
1293 oscar_reformat_screenname(gaim_account_get_connection(account), [inFormattedUID UTF8String]);
1298 #pragma mark Request callbacks
1300 - (void)performContactMenuActionFromDict:(NSDictionary *)dict
1302 GaimMenuAction *act = [[dict objectForKey:@"GaimMenuAction"] pointerValue];
1303 GaimBuddy *buddy = [[dict objectForKey:@"GaimBuddy"] pointerValue];
1305 //Perform act's callback with the desired buddy and data
1307 ((void (*)(void *, void *))act->callback)((GaimBlistNode *)buddy, act->data);
1310 - (void)performAccountMenuActionFromDict:(NSDictionary *)dict
1312 GaimPluginAction *pam = [[dict objectForKey:@"GaimPluginAction"] pointerValue];
1319 * @brief Call the gaim callback to finish up the window
1321 * @param inCallBackValue The cb to use
1322 * @param inUserDataValue Original user data
1324 - (void)doAuthRequestCbValue:(NSValue *)inCallBackValue withUserDataValue:(NSValue *)inUserDataValue
1326 GaimAccountRequestAuthorizationCb callBack = [inCallBackValue pointerValue];
1328 callBack([inUserDataValue pointerValue]);
1332 #pragma mark Secure messaging
1334 - (void)gaimConversation:(GaimConversation *)conv setSecurityDetails:(NSDictionary *)securityDetailsDict
1338 - (void)refreshedSecurityOfGaimConversation:(GaimConversation *)conv
1340 GaimDebug (@"*** Refreshed security...");
1345 gaim_signals_disconnect_by_handle(adium_gaim_get_handle());
1351 //This doesn't work for several reasons. The biggest: libgaim expects strings to be translated immediately;
1352 //substitutions have already occurred, as of concatenations, because we see them.
1353 #pragma mark Translation
1355 - (NSString *)localizedGaimString:(NSString *)inString
1357 static BOOL configuredGettext = NO;
1358 if (!configuredGettext) {
1359 bindtextdomain("libgaim", [[[[NSBundle bundleForClass:[self class]] resourcePath] stringByAppendingPathComponent:@"potfiles"] UTF8String]);
1360 bind_textdomain_codeset("libgaim", "UTF-8");
1363 NSString *preferredLocalization = [[[NSBundle mainBundle] preferredLocalizations] objectAtIndex:0];
1364 setenv("LANGUAGE", [preferredLocalization UTF8String], 1);
1365 AILog(@"Gaim translation using %s",[preferredLocalization UTF8String]);
1367 //Make change known. _nl_msg_cat_cntr is an external defined in gettext's loadmsgcat.c
1369 extern int _nl_msg_cat_cntr;
1374 return [NSString stringWithUTF8String:dgettext("libgaim", [inString UTF8String])];